﻿using System;
using System.Data;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Configuration;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;

using Microsoft.SqlServer.Types;
using System.Data.Sql;
using System.Data.SqlClient;

namespace MapViewer
{
    //-------------------------------------------------------------------------
    // Hold values that we'll use to generate SQL "where" clause
    //-------------------------------------------------------------------------
    public class LayerValueList : List<Parameter>
    {
        // build SQL "where" clause by 'and'ing together all our items in the list
        internal String BuildFilter()
        {
            if (0 == this.Count)
                return String.Empty;

            StringBuilder filter = new StringBuilder();

            filter.Append(" where ");
            foreach (Parameter item in this)
            {
                filter.Append(item.Name + " = @" + item.Name + " and ");
            }

            return filter.ToString().Substring(0, filter.ToString().Length-5);
        }

        // determine if a data row matches the current selection
        internal bool Match(DataRow row)
        {
            if (0 == this.Count) 
                return false;

            bool match = true;

            foreach (Parameter item in this)
            {
                match &= (row[item.Name].ToString() == item.DefaultValue);
            }

            return match;
        }
    }

    //-------------------------------------------------------------------------
    // Manage the layers and layer information 
    //-------------------------------------------------------------------------
    internal class LayerManager : KeyedCollection<String, LayerInfo>
    {
        // required override for keyedcollection. Defines the item field that
        // is used for the collection key
        protected override string GetKeyForItem(LayerInfo item)
        {
            return item.name;
        }

        public LayerManager()
        {
            // set defaults
            EnvelopeBoundary = 1.5F;
            minimumEnvelopeSize = 1;
        }

        //---------------------------------------------------------------------
        //---------------------------------------------------------------------
        // Public Interface - attributes and methods
        //---------------------------------------------------------------------
        //---------------------------------------------------------------------
        
        // override add a layer to replace a same-named layer, since KeyedCollection 
        // can't have duplicate keys.
        public void Add(String name, LayerInfo layer)
        {
            if (this.Contains(name))
            {
                this.Remove(name);
            }

            layer.manager = this;
            layer.name = name;
            base.Add(layer);
        }

        // identify layer to use for data selection by key
        public String SelectionLayerName { get; set; }
        public LayerInfo SelectionLayer()
        {
            if (this.Contains(SelectionLayerName))
            {
                return this[SelectionLayerName];
            }

            throw new Exception("No Selection Layer Defined");
        }

        // Get the area around identified selection values for display. This actually
        // queries the selected layer and returns the envelope that will be displayed
        public SqlGeometry GetSelectionRegion(LayerValueList selection)
        {
            Envelope = SelectionLayer().getEnvelopeByKey(selection);
            return Envelope;
        }

        // Envelope that will be displayed. This is the region that will be selected from
        // all layers to make up our image. Set by querying the selection layer in GetSelectionRegion
        private SqlGeometry _envelope;
        public SqlGeometry Envelope
        {
            get { return _envelope; }
            set { _envelope = value; }
        }

        // Minimum size of envelope. Used by GetSelectionRegion to determine the smallest
        // envelope around the selected polygon.
        public float minimumEnvelopeSize
        { get; set; }

        // Ratio to determine how big of an envelope around the selected polygon(s) will be 
        // retrieved in GetSelectionRegion
        public float EnvelopeBoundary
        { get; set; }

        // Create an envelope from a standard text representation. We do this to 
        // encapsulate all the nasty type conversions needed.
        public SqlGeometry EnvelopeFromText(String env_text)
        {
            _envelope = SqlGeometry.STGeomFromText(new System.Data.SqlTypes.SqlChars(env_text.ToCharArray()), 0);
            return _envelope;
        }

        // calculate the ratio between the size of the geometry envelope and the output image
        internal float GetOutputScale()
        {
            float width = (float)(Envelope.STPointN(3).STX - Envelope.STPointN(1).STX);
            float height = (float)(Envelope.STPointN(3).STY - Envelope.STPointN(1).STY);

            float scale = height > width ? outputsize.Width / height : outputsize.Height / width;
            return scale;
        }

        // Size of output drawing the width and height of the output bitmap
        // duplicated here for internal visibility, set through MapViewer_Control object
        internal Size outputsize
        { get; set; }
    }

    //-------------------------------------------------------------------------
    // Abstract base class for layer processing. Specific layers based on different
    // data access methods are build on top of this class.
    //-------------------------------------------------------------------------
    public abstract class LayerInfo
    {
        // Color management for this layer
        public class LayerDrawing
        {
            // Types of color processing we support
            public enum ColorProcessTypes
            {
                None,    // no body color
                Random,  // random body color
                Legend   // user provided dictionary of colors
            };

            // How to process color for this layer
            public ColorProcessTypes ColorType { get; set; }

            // Field name in the data that contains the color value
            // public String ColorField { get; set; }
            
            // User provided color legend. This will be searched for 
            // data[keycolumn] value to get the associated color
            public Dictionary<String, Color> ColorLegend { get; set; }

            // Shape outline pen
            public Pen LinePen { get; set; }

            // Body color for selected shapes
            public Brush SelectedBodyBrush { get; set; }

            // Outline 
            public Pen SelectedLinePen { get; set; }

            // Font for displaying key values in the image
            public Font DisplayFont { get; set; }

            // Text color
            public Brush TextBrush { get; set; }

            // When to turn off text drawing. the larger the number, the bigger the base
            // area before drawing text is turned off.
            public int TextClip { get; set; }

            // Constructor - Set default values
            public LayerDrawing()
            {
                ColorType = ColorProcessTypes.Random;
                LinePen = new Pen(Color.Gray, 2);

                SelectedLinePen = new Pen(Color.Red, 2);

                SelectedBodyBrush = new HatchBrush(HatchStyle.ForwardDiagonal,
                    SelectedLinePen.Color, Color.FromArgb(33, Color.White));

                DisplayFont = new Font("Arial", 10);
                TextBrush = Brushes.Black;
                TextClip = 20;
            }

            // Get a brush with the correct color for this region
            internal Brush GetBrush(String key)
            {
                Brush bodybrush;
                switch (ColorType)
                {
                    // No color is white
                    case ColorProcessTypes.None:
                        bodybrush = new SolidBrush(Color.FromArgb(33, Color.White));
                        break;
                    // If the caller provided a list of colors, look up the correct color
                    case ColorProcessTypes.Legend:
                        if (ColorLegend.ContainsKey(key))
                            bodybrush = new SolidBrush(Color.FromArgb(33, ColorLegend[key]));
                        else
                            bodybrush = new SolidBrush(Color.FromArgb(33, Color.White));
                        break;
                    // If we're generating random colors, get the next in line
                    case ColorProcessTypes.Random:
                        bodybrush = new SolidBrush(ColorPlus.Random);
                        break;
                    default:
                        throw new Exception("Color processing not defined");
                }

                return bodybrush;
            }
        }

        // Hold the column name and display value for attributes that will be displayed
        // in the mouse-over text box
        internal struct DisplayColumnDetails
        {
            internal String ColumnName;
            internal String DisplayName;
            internal DisplayColumnDetails(String cn, String dn)
            {
                ColumnName = cn;
                DisplayName = dn;
            }
        }

        // diagnostic
        internal DateTime start;
        internal DateTime stop;
        internal TimeSpan duration
        {
            get { return stop - start; }
        }
        
        //---------------------------------------------------------------------
        //---------------------------------------------------------------------
        // Public Attributes
        //---------------------------------------------------------------------
        //---------------------------------------------------------------------

        // Name of this layer - this is the layer's unique key
        public String name
        { get; set; }

        // Key column name. Used for querying data and the selected value in this 
        // column will be highlighted in the resulting image.
        public String keycolumn
        { get; set; }

        // Whether or not to write the key column value into the image.
        public Boolean displaykey
        { get; set; }

        // Write a key column value for each geometry or once per key value
        public Boolean condensekey
        { get; set; }

        // Geometry column to draw. This is the name of the geometry column in the database table
        public String geoColumn
        { get; set; }

        // columns to display in sidebar
        internal List<DisplayColumnDetails> DisplayColumns = new List<DisplayColumnDetails>();
        public void AddDisplayColumn(String column, String displayname)
        { DisplayColumns.Add(new DisplayColumnDetails(column, displayname)); }

        // handle how to color shapes in this layer
        public LayerDrawing LayerDrawingOptions = new LayerDrawing();

        //---------------------------------------------------------------------
        // Data Retrieval. These two methods must be supplied by any subclasses
        //---------------------------------------------------------------------
        
        // Take in a selection value and return the region or envelope that we 
        // will be displaying in the image.
        public abstract SqlGeometry getEnvelopeByKey(LayerValueList selection);

        // Get the records from the database table that intersect the envelope
        // returned in getEnvelopeByKey
        public abstract DataTable getByRegion();

        // Error messages querying data
        internal List<String> _errors = new List<string>();
        public List<String> errors
        {
            get { return _errors; } 
        }

        //---------------------------------------------------------------------
        //---------------------------------------------------------------------
        // Internal attributes and methods
        //---------------------------------------------------------------------
        //---------------------------------------------------------------------
        
        // hold pointer to the parent manager class
        internal LayerManager manager
        { get; set; }

        // hold a list of element identifiers for selected items
        internal List<String> selecteditems = new List<String>();

        // count the number of geometry records processed
        internal int geometrycount
        { get; set; }
    }

    //-------------------------------------------------------------------------
    //-------------------------------------------------------------------------
    // Layers. These classes implement the layer abstract class, providing
    // the data interface and layer specific data items
    //-------------------------------------------------------------------------
    //-------------------------------------------------------------------------

    //-------------------------------------------------------------------------
    // Get data from a connection string, table and column names
    //-------------------------------------------------------------------------
    public class SqlQueryLayer : LayerInfo
    {
        // Region values for get by region 
        public float top
        { get; set; }
        public float bottom
        { get; set; }
        public float left
        { get; set; }
        public float right
        { get; set; }

        // Sql connection
        public String sqlconnection
        { get; set; }

        // Table name 
        public String tablename
        { get; set; }

        // Tables geometry column
        private String _tablegeocolumn;
        public String tablegeocolumn
        {
            get { return _tablegeocolumn; }
            set { _tablegeocolumn = value; geoColumn = "i_" + _tablegeocolumn; }
        }

        // get data from sql server by key value comparison
        public override SqlGeometry getEnvelopeByKey(LayerValueList selection)
        {
            // diagnostic
            start = DateTime.Now;

            // reset selected items list
            selecteditems.Clear();

            // hold returned data
            DataTable data = new DataTable();
            
            // build the command
            SqlConnection conn = new SqlConnection(sqlconnection);
            SqlCommand cmd = new SqlCommand();
            cmd.Connection = conn;

            cmd.CommandText = "select " + keycolumn + ", " + tablegeocolumn + " ";
            cmd.CommandText += "from " + tablename; // +" where " + keycolumn + " = @1"; 

            cmd.CommandText += selection.BuildFilter();
            foreach (Parameter item in selection)
            {
                cmd.Parameters.Add(new SqlParameter(item.Name, item.DefaultValue));
            }

            // Go get the data 
            try
            {
                conn.Open();
                SqlDataReader rd = cmd.ExecuteReader();
                data = new DataTable();
                data.Load(rd);
                conn.Close();
            }
            catch (SqlException sqlx)
            {
                // set diagnostic message in error list
                _errors.Add(String.Format("Sql Error ({0}) querying key data from {1}.", sqlx.Message, tablename));
            }

            if (0 == data.Rows.Count)
            {
                stop = DateTime.Now;
                return SqlGeometry.Null;
            }
            else 
            {
                SqlGeometry bounds = null;
                foreach (DataRow row in data.Rows)
                {
                    if (!row.IsNull(tablegeocolumn))
                    {
                        // save selected key attribute
                        selecteditems.Add(row[keycolumn].ToString());

                        // get the geometry field into .NET variable
                        SqlGeometry g = (SqlGeometry)row[tablegeocolumn];

                        // collect bounding box data
                        if (null == bounds)
                            bounds = g.STEnvelope();
                        else
                            if (g.STIsValid())
                                bounds = bounds.STUnion(g).STEnvelope();
                            else
                            {
                                bounds = bounds.STUnion(g.MakeValid()).STEnvelope();
                            }
                    }
                }

                // get coordinates
                top = (float)bounds.STPointN(1).STY;
                left = (float)bounds.STPointN(1).STX;
                bottom = (float)bounds.STPointN(3).STY;
                right = (float)bounds.STPointN(3).STX;

                // expand region keeping the current center, with a minimum size of 500 units
                float centerx = left + (right - left) / 2;
                float centery = top + (bottom - top) / 2;
                
                float delta = Math.Max(bottom - top, right - left);
                delta = delta < manager.minimumEnvelopeSize ? manager.minimumEnvelopeSize : delta;

                top = centery - manager.EnvelopeBoundary * delta;
                bottom = centery + manager.EnvelopeBoundary * delta;
                left = centerx - manager.EnvelopeBoundary * delta;
                right = centerx + manager.EnvelopeBoundary * delta;

                // build polygon text from new points
                StringBuilder poly = new StringBuilder();
                poly.Append("POLYGON((");
                poly.Append(left.ToString()).Append(" ").Append(top.ToString()).Append(", ");
                poly.Append(right.ToString()).Append(" ").Append(top.ToString()).Append(", ");
                poly.Append(right.ToString()).Append(" ").Append(bottom.ToString()).Append(", ");
                poly.Append(left.ToString()).Append(" ").Append(bottom.ToString()).Append(", ");
                poly.Append(left.ToString()).Append(" ").Append(top.ToString()).Append(" ");
                poly.Append("))");

                // diagnostic
                stop = DateTime.Now;

                // ... and finally, return the new bounding box as our query envelope
                return SqlGeometry.STGeomFromText(new System.Data.SqlTypes.SqlChars(poly.ToString().ToCharArray()), 0);
            }
        }

        // get data from sql server by region intersection
        public override DataTable getByRegion()
        {
            // diagnostic
            start = DateTime.Now;

            DataTable data = new DataTable();

            // if we have no envelope to search within, return an empty data table.
            if (manager.Envelope.IsNull)
            {
                // If we had a Null Envelope, don't bother querying...
                stop = DateTime.Now;
                return data;
            }

            // get scaling factor
            float scale = manager.GetOutputScale();

            // build the command
            SqlConnection conn = new SqlConnection(sqlconnection);
            SqlCommand cmd = new SqlCommand();
            cmd.Connection = conn;

            String text = "declare @env geometry; ";
            text += "set @env = geometry::STGeomFromText('" + manager.Envelope.ToString() + "', 0).MakeValid().STEnvelope(); ";
            text += "select ";
            text += keycolumn + ", ";
            foreach (LayerInfo.DisplayColumnDetails col in DisplayColumns) text += col.ColumnName + ", ";
            //text += "@env.STIntersection(" + tablegeocolumn + ").Reduce(" + scale.ToString() + ") as " + geoColumn + ",";
            text += "@env.STIntersection(" + tablegeocolumn + ") as " + geoColumn + ",";
            text += "@env.STIntersection(" + tablegeocolumn + ").STArea() as geo_Area ";
            text += "from " + tablename + " where " + tablegeocolumn+".STIntersects(@env) = 1 ";
            text += "order by " + tablename + "." + keycolumn;

            cmd.CommandText = text;

            // Go get the data 
            try
            {
                conn.Open();
                SqlDataReader rd = cmd.ExecuteReader();
                data = new DataTable();
                data.Load(rd);
                conn.Close();
                geometrycount = data.Rows.Count;
            }
            catch (SqlException sqlx)
            {
                // set diagnostic message in error list
                _errors.Add(String.Format("Sql Error ({0}) querying region data from {1}.", sqlx.Message, tablename));
            }

            if (0 == data.Rows.Count)
            {
                data.Clear();
            }

            // diagnostic
            stop = DateTime.Now;

            return data;
        }
    }

    //-------------------------------------------------------------------------
    // Get Data from a SqlDataSource.
    //-------------------------------------------------------------------------
    public class SqlDataSourceLayer : LayerInfo
    {
        public SqlDataSource source
        { get; set; }

        // Region values for get by region 
        internal float top
        { get; set; }
        internal float bottom
        { get; set; }
        internal float left
        { get; set; }
        internal float right
        { get; set; }

        // Table name 
        public String tablename
        { get; set; }

        // Tables geometry column, also set geocolumn value
        private String _tablegeocolumn;
        public String tablegeocolumn
        {
            get { return _tablegeocolumn; }
            set { _tablegeocolumn = value; geoColumn = "i_" + _tablegeocolumn; }
        }

        // get envelope using key value
        public override SqlGeometry getEnvelopeByKey(LayerValueList selection)
        {
            // diagnostic
            start = DateTime.Now;

            // reset selected items list
            selecteditems.Clear();

            String saveselect = source.SelectCommand;

            //source.SelectCommand += " where " + keycolumn + " = @" + keycolumn;
            //source.SelectParameters.Add(keycolumn, keyvalue);

            source.SelectCommand += selection.BuildFilter();
            foreach (Parameter item in selection)
            {
                source.SelectParameters.Add(item);
            }

            DataTable data = new DataTable();
            try
            {
                if (source.DataSourceMode == SqlDataSourceMode.DataReader)
                    data.Load((IDataReader)source.Select(DataSourceSelectArguments.Empty));
                else
                {
                    DataView dv = (DataView)source.Select(DataSourceSelectArguments.Empty);
                    data = dv.Table;
                }
            }
            catch (SqlException sqlx)
            {
                // set diagnostic message in error list
                _errors.Add(String.Format("Sql Error ({0}) querying key data from {1}.", sqlx.Message, tablename));
            }
            catch (Exception ex)
            {
                // set diagnostic message in error list
                _errors.Add(String.Format("Invalid Inputs, error is ({0}) - missing key data?", ex.Message));
            }
            finally
            {
                source.SelectCommand = saveselect;
                source.SelectParameters.Clear();

                // diagnostic
                stop = DateTime.Now;
            }

            if (0 == data.Rows.Count)
            {
                // If no data, return a null geometry
                stop = DateTime.Now;
                return SqlGeometry.Null;
            }

            else
            {
                SqlGeometry bounds = null;
                foreach (DataRow row in data.Rows)
                {
                    if (!row.IsNull(tablegeocolumn))
                    {
                        // save selected key attribute
                        selecteditems.Add(row[keycolumn].ToString());

                        // get the geometry field into .NET variable
                        SqlGeometry g = (SqlGeometry)row[tablegeocolumn];

                        // collect bounding box data
                        if (null == bounds)
                            bounds = g.STEnvelope();
                        else
                            if (g.STIsValid())
                                bounds = bounds.STUnion(g).STEnvelope();
                            else
                            {
                                bounds = bounds.STUnion(g.MakeValid()).STEnvelope();
                            }
                    }
                }

                // get coordinates
                top = (float)bounds.STPointN(1).STY;
                left = (float)bounds.STPointN(1).STX;
                bottom = (float)bounds.STPointN(3).STY;
                right = (float)bounds.STPointN(3).STX;

                // expand region keeping the current center, with a minimum size of 500 units
                float centerx = left + (right - left) / 2;
                float centery = top + (bottom - top) / 2;

                float delta = Math.Max(bottom - top, right - left);
                delta = delta < manager.minimumEnvelopeSize ? manager.minimumEnvelopeSize : delta;

                top = centery - manager.EnvelopeBoundary * delta;
                bottom = centery + manager.EnvelopeBoundary * delta;
                left = centerx - manager.EnvelopeBoundary * delta;
                right = centerx + manager.EnvelopeBoundary * delta;

                // build polygon text from new points
                StringBuilder poly = new StringBuilder();
                poly.Append("POLYGON((");
                poly.Append(left.ToString()).Append(" ").Append(top.ToString()).Append(", ");
                poly.Append(right.ToString()).Append(" ").Append(top.ToString()).Append(", ");
                poly.Append(right.ToString()).Append(" ").Append(bottom.ToString()).Append(", ");
                poly.Append(left.ToString()).Append(" ").Append(bottom.ToString()).Append(", ");
                poly.Append(left.ToString()).Append(" ").Append(top.ToString()).Append(" ");
                poly.Append("))");

                // diagnostic
                stop = DateTime.Now;

                // ... and finally, return the new bounding box as our query envelope
                return SqlGeometry.STGeomFromText(new System.Data.SqlTypes.SqlChars(poly.ToString().ToCharArray()), 0);
            }
        }

                // get data from sql server by region intersection
        public override DataTable getByRegion()
        {
            // diagnostic
            start = DateTime.Now;
            
            String saveselect = source.SelectCommand;

            DataTable data = new DataTable();

            // if we have no envelope to search within, return an empty data table.
            if (manager.Envelope.IsNull)
            {
                // If we have a null envelope, don't bother querying ...
                stop = DateTime.Now;
                return data;               
            }

            // get scaling factor
            float scale = manager.GetOutputScale();

            String text = "declare @env geometry; ";
            text += "set @env = geometry::STGeomFromText('" + manager.Envelope.ToString() + "', 0).MakeValid().STEnvelope(); ";
            text += "select ";
            text += keycolumn + ", ";
            foreach (LayerInfo.DisplayColumnDetails col in DisplayColumns) text += col.ColumnName + ", ";
            text += "@env.STIntersection(" + tablegeocolumn + ") as " + geoColumn + ",";
            text += "@env.STIntersection(" + tablegeocolumn + ").STArea() as geo_Area ";
            text += "from " + tablename + " where " + tablegeocolumn + ".STIntersects(@env) = 1 ";
            text += "order by " + tablename + "." + keycolumn;

            source.SelectCommand = text;

            // Go get the data 
            try
            {
                if (source.DataSourceMode == SqlDataSourceMode.DataReader)
                    data.Load((IDataReader)source.Select(DataSourceSelectArguments.Empty));
                else
                {
                    DataView dv = (DataView)source.Select(DataSourceSelectArguments.Empty);
                    data = dv.Table;
                    geometrycount = data.Rows.Count;
                }
            }
            catch (SqlException sqlx)
            {
                // set diagnostic message in error list
                _errors.Add(String.Format("Sql Error ({0}) querying region data from {1}.", sqlx.Message, tablename));
            }
            finally
            {
                // Stop the process clock
                stop = DateTime.Now;
                source.SelectCommand = saveselect;
            }

            if (0 == data.Rows.Count)
            {
                data.Clear();
            }

            source.SelectCommand = saveselect;

            // Stop the clock and return our data
            stop = DateTime.Now;
            return data;
        }
    }
}
